// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/plugin_data_remover_impl.h"

#include <stdint.h>

#include <limits>

#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequenced_task_runner_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/version.h"
#include "build/build_config.h"
#include "content/browser/plugin_service_impl.h"
#include "content/browser/renderer_host/pepper/pepper_flash_file_message_filter.h"
#include "content/common/child_process_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/child_process_host.h"
#include "content/public/common/content_constants.h"
#include "content/public/common/pepper_plugin_info.h"
#include "ppapi/proxy/ppapi_messages.h"

namespace content {

namespace {

    // The minimum Flash Player version that implements NPP_ClearSiteData.
    const char kMinFlashVersion[] = "10.3";
    const int64_t kRemovalTimeoutMs = 10000;
    const uint64_t kClearAllData = 0;

} // namespace

// static
PluginDataRemover* PluginDataRemover::Create(BrowserContext* browser_context)
{
    return new PluginDataRemoverImpl(browser_context);
}

// static
void PluginDataRemover::GetSupportedPlugins(
    std::vector<WebPluginInfo>* supported_plugins)
{
    bool allow_wildcard = false;
    std::vector<WebPluginInfo> plugins;
    PluginService::GetInstance()->GetPluginInfoArray(
        GURL(), kFlashPluginSwfMimeType, allow_wildcard, &plugins, NULL);
    base::Version min_version(kMinFlashVersion);
    for (std::vector<WebPluginInfo>::iterator it = plugins.begin();
         it != plugins.end(); ++it) {
        base::Version version;
        WebPluginInfo::CreateVersionFromString(it->version, &version);
        if (version.IsValid() && min_version.CompareTo(version) == -1)
            supported_plugins->push_back(*it);
    }
}

class PluginDataRemoverImpl::Context
    : public PpapiPluginProcessHost::BrokerClient,
      public IPC::Listener,
      public base::RefCountedThreadSafe<Context,
          BrowserThread::DeleteOnIOThread> {
public:
    Context(base::Time begin_time, BrowserContext* browser_context)
        : event_(new base::WaitableEvent(
            base::WaitableEvent::ResetPolicy::MANUAL,
            base::WaitableEvent::InitialState::NOT_SIGNALED))
        , begin_time_(begin_time)
        , is_removing_(false)
        , browser_context_path_(browser_context->GetPath())
    {
        DCHECK_CURRENTLY_ON(BrowserThread::UI);
    }

    void Init(const std::string& mime_type)
    {
        BrowserThread::PostTask(
            BrowserThread::IO,
            FROM_HERE,
            base::Bind(&Context::InitOnIOThread, this, mime_type));
        BrowserThread::PostDelayedTask(
            BrowserThread::IO,
            FROM_HERE,
            base::Bind(&Context::OnTimeout, this),
            base::TimeDelta::FromMilliseconds(kRemovalTimeoutMs));
    }

    void InitOnIOThread(const std::string& mime_type)
    {
        PluginServiceImpl* plugin_service = PluginServiceImpl::GetInstance();

        // Get the plugin file path.
        std::vector<WebPluginInfo> plugins;
        plugin_service->GetPluginInfoArray(
            GURL(), mime_type, false, &plugins, NULL);

        if (plugins.empty()) {
            // May be empty for some tests and on the CrOS login OOBE screen.
            event_->Signal();
            return;
        }

        base::FilePath plugin_path = plugins[0].path;

        PepperPluginInfo* pepper_info = plugin_service->GetRegisteredPpapiPluginInfo(plugin_path);
        if (!pepper_info) {
            event_->Signal();
            return;
        }

        DCHECK_CURRENTLY_ON(BrowserThread::IO);
        remove_start_time_ = base::Time::Now();
        is_removing_ = true;

        // Balanced in OnPpapiChannelOpened.
        AddRef();
        plugin_name_ = pepper_info->name;
        // Use the broker since we run this function outside the sandbox.
        plugin_service->OpenChannelToPpapiBroker(0, plugin_path, this);
    }

    // Called when a timeout happens in order not to block the client
    // indefinitely.
    void OnTimeout()
    {
        LOG_IF(ERROR, is_removing_) << "Timed out";
        SignalDone();
    }

    bool Incognito() override { return false; }

    // PpapiPluginProcessHost::BrokerClient implementation.
    void GetPpapiChannelInfo(base::ProcessHandle* renderer_handle,
        int* renderer_id) override
    {
        *renderer_handle = base::kNullProcessHandle;
        *renderer_id = 0;
    }

    void OnPpapiChannelOpened(const IPC::ChannelHandle& channel_handle,
        base::ProcessId /* peer_pid */,
        int /* child_id */) override
    {
        if (channel_handle.is_mojo_channel_handle())
            ConnectToChannel(channel_handle);

        // Balancing the AddRef call.
        Release();
    }

    // IPC::Listener methods.
    bool OnMessageReceived(const IPC::Message& message) override
    {
        IPC_BEGIN_MESSAGE_MAP(Context, message)
        IPC_MESSAGE_HANDLER(PpapiHostMsg_ClearSiteDataResult,
            OnPpapiClearSiteDataResult)
        IPC_MESSAGE_UNHANDLED_ERROR()
        IPC_END_MESSAGE_MAP()

        return true;
    }

    void OnChannelError() override
    {
        if (is_removing_) {
            NOTREACHED() << "Channel error";
            SignalDone();
        }
    }

    base::WaitableEvent* event() { return event_.get(); }

private:
    friend struct BrowserThread::DeleteOnThread<BrowserThread::IO>;
    friend class base::DeleteHelper<Context>;
    ~Context() override { }

    IPC::Message* CreatePpapiClearSiteDataMsg(uint64_t max_age)
    {
        base::FilePath profile_path = PepperFlashFileMessageFilter::GetDataDirName(browser_context_path_);
        // TODO(vtl): This "duplicates" logic in webkit/plugins/ppapi/file_path.cc
        // (which prepends the plugin name to the relative part of the path
        // instead, with the absolute, profile-dependent part being enforced by
        // the browser).
#if defined(OS_WIN)
        base::FilePath plugin_data_path = profile_path.Append(base::FilePath(base::UTF8ToUTF16(plugin_name_)));
#else
        base::FilePath plugin_data_path = profile_path.Append(base::FilePath(plugin_name_));
#endif // defined(OS_WIN)
        return new PpapiMsg_ClearSiteData(0u, plugin_data_path, std::string(),
            kClearAllData, max_age);
    }

    // Connects the client side of a newly opened plugin channel.
    void ConnectToChannel(const IPC::ChannelHandle& handle)
    {
        DCHECK_CURRENTLY_ON(BrowserThread::IO);

        // If we timed out, don't bother connecting.
        if (!is_removing_)
            return;

        DCHECK(!channel_.get());
        channel_ = IPC::Channel::CreateClient(handle, this);
        if (!channel_->Connect()) {
            NOTREACHED() << "Couldn't connect to plugin";
            SignalDone();
            return;
        }

        uint64_t max_age = begin_time_.is_null()
            ? std::numeric_limits<uint64_t>::max()
            : (base::Time::Now() - begin_time_).InSeconds();

        IPC::Message* msg = CreatePpapiClearSiteDataMsg(max_age);
        if (!channel_->Send(msg)) {
            NOTREACHED() << "Couldn't send ClearSiteData message";
            SignalDone();
            return;
        }
    }

    // Handles the PpapiHostMsg_ClearSiteDataResult message.
    void OnPpapiClearSiteDataResult(uint32_t request_id, bool success)
    {
        DCHECK_EQ(0u, request_id);
        LOG_IF(ERROR, !success) << "ClearSiteData returned error";
        UMA_HISTOGRAM_TIMES("ClearPluginData.time",
            base::Time::Now() - remove_start_time_);
        SignalDone();
    }

    // Signals that we are finished with removing data (successful or not). This
    // method is safe to call multiple times.
    void SignalDone()
    {
        DCHECK_CURRENTLY_ON(BrowserThread::IO);
        if (!is_removing_)
            return;
        is_removing_ = false;
        event_->Signal();
    }

    std::unique_ptr<base::WaitableEvent> event_;
    // The point in time when we start removing data.
    base::Time remove_start_time_;
    // The point in time from which on we remove data.
    base::Time begin_time_;
    bool is_removing_;

    // Path for the current profile. Must be retrieved on the UI thread from the
    // browser context when we start so we can use it later on the I/O thread.
    base::FilePath browser_context_path_;

    // The name of the plugin. Use only on the I/O thread.
    std::string plugin_name_;

    // The channel is NULL until we have opened a connection to the plugin
    // process.
    std::unique_ptr<IPC::Channel> channel_;
};

PluginDataRemoverImpl::PluginDataRemoverImpl(BrowserContext* browser_context)
    : mime_type_(kFlashPluginSwfMimeType)
    , browser_context_(browser_context)
{
}

PluginDataRemoverImpl::~PluginDataRemoverImpl()
{
}

base::WaitableEvent* PluginDataRemoverImpl::StartRemoving(
    base::Time begin_time)
{
    DCHECK(!context_.get());
    context_ = new Context(begin_time, browser_context_);
    context_->Init(mime_type_);
    return context_->event();
}

} // namespace content
